[Redux] Redux 實作

前言

前陣子寫的Redux解析感覺寫不清楚,因此重寫一篇如何實作Redux

這邊必須對Redux有些基本認識,而實作的部分都先不做錯誤處理。


createStore

首先從createStore的部分開始,在實做createStore前先如同往常寫好一個reducer並把他傳入createStore中,並對他發出一個有效的action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const reducer = ( state = 0 , action ) => {
switch ( action.type ){
case 'ADD':
return state + action.payload ;
default :
return state ;
}
}
let store = createStore(reducer) ;
console.log(store.getState()); // 0
store.dispatch({
type : 'ADD',
payload : 1
});
console.log(store.getState()); // 1

接下來開始實做createStore,首先先建立getStatedispatch函式。

1
2
3
4
5
6
7
8
9
10
11
12
const createStore = () => {
const getState = () => {
}
const dispatch = () => {
}
return {
getState ,
dispatch
}
}

接下來紀錄傳進來的reducerstate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const createStore = (reducer,state) => {
let nowReducer = reducer ;
let nowState = state ;
const getState = () => {
}
const dispatch = () => {
}
return {
getState ,
dispatch
}
}

getState

getState只要簡單回傳nowState即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const createStore = (reducer,state) => {
let nowReducer = reducer ;
let nowState = state ;
const getState = () => {
return nowState ;
}
const dispatch = () => {
}
return {
getState ,
dispatch
}
}

dispatch

dispatch接受action把它丟給reducer處理,並將nowState替換成運算後的state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const createStore = (reducer,state) => {
let nowReducer = reducer ;
let nowState = state ;
const getState = () => {
return nowState ;
}
const dispatch = (action) => {
nowState = nowReducer(nowState,action) ;
}
return {
getState ,
dispatch
}
}

為了有初始狀態,先發送一個不會被處理的action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const createStore = (reducer,state) => {
let nowReducer = reducer ;
let nowState = state ;
const getState = () => {
return nowState ;
}
const dispatch = (action) => {
nowState = nowReducer(nowState,action) ;
}
dispatch({ type : 'INIT' }) ;
return {
getState ,
dispatch
}
}

如此一來簡單的createStore就可以運行了。

subscribe

subscribe讓你可以讓你綁定函式在dispatch後執行,同時回傳一個unsubscribe函式讓你可以取消綁定。

1
2
3
4
5
6
7
8
9
10
const listener = () => {
console.log('Now state: ',store.getState()) ;
}
let store = createStore(reducer) ;
let unsubscribe = store.subscribe(listener) ;
store.dispatch({ type : 'ADD' , payload : 1 });
unsubscribe();
store.dispatch({ type : 'ADD' , payload : 1 });
console.log(store.getState());

首先額外建立一個陣列來保存subscribe的所有函式,並建立subscribe函式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const createStore = (reducer,state) => {
let nowReducer = reducer ;
let nowState = state ;
let nowListener = [] ;
const getState = () => {
return nowState ;
}
const dispatch = (action) => {
nowState = nowReducer(nowState,action) ;
}
const subscribe = () => {
}
dispatch({ type : 'INIT' }) ;
return {
getState ,
dispatch ,
subscribe
}
}

接下來在只要註冊事件就把該函式放入nowListener並回傳一個函式用來註銷。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const createStore = (reducer,state) => {
let nowReducer = reducer ;
let nowState = state ;
let nowListeners = [] ;
const getState = () => {
return nowState ;
}
const dispatch = (action) => {
nowState = nowReducer(nowState,action) ;
nowListeners.forEach((listener) => listener());
}
const subscribe = (listener) => {
nowListeners.push(listener) ;
return function(){
nowListeners.splice(nowListeners.indexOf(listener),1);
}
}
dispatch({ type : 'INIT' }) ;
return {
getState ,
dispatch ,
subscribe
}
}

Reduxsubscribe部分會維持兩個陣列,一個是上次dispatch後的註冊事件,另一個則是在下次dispatch前新註冊的所有事件,這兩個事件陣列會在dispatch後同步,在這邊不實作這部分。

replaceReducer

將傳進來的reducer和目前的reducer替換

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const reducer = ( state = 0 , action ) => {
switch ( action.type ){
case 'ADD':
return state + action.payload ;
default :
return state ;
}
}
const newReducer = ( state = 0 , action ) => {
switch ( action.type ){
case 'ADD':
return state + action.payload * 2 ;
default :
return state ;
}
}
let store = createStore(reducer) ;
store.dispatch({ type : 'ADD' , payload : 1 });
store.replaceReducer(newReducer);
store.dispatch({ type : 'ADD' , payload : 1 });
console.log(store.getState()); // 3

只要把nowReducer替換成傳進來的reducer即可,替換後要發出初始action來獲得初始狀態。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const createStore = (reducer,state) => {
let nowReducer = reducer ;
let nowState = state ;
let nowListeners = [] ;
const getState = () => {
return nowState ;
}
const dispatch = (action) => {
nowState = nowReducer(nowState,action) ;
nowListeners.forEach((listener) => listener());
}
const subscribe = (listener) => {
nowListeners.push(listener) ;
return function(){
nowListeners.splice(nowListeners.indexOf(listener),1);
}
}
const replaceReducer = (reducer) => {
nowReducer = reducer ;
dispatch({ type : 'INIT' }) ;
}
dispatch({ type : 'INIT' }) ;
return {
getState ,
dispatch ,
subscribe ,
replaceReducer
}
}

如此一來基本的createStore就實作完成了,接下來實作combineReducer的部分。


combineReducer

由於reducer可能不只一個,因此必須將reducers合併為一個。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const personReducer = ( state = {} , action ) => {
switch ( action.type ){
case 'NAME':
return {...state, name : action.payload };
case 'AGE':
return {...state, age : action.payload } ;
default :
return state ;
}
}
const todoReducer = ( state = [] , action ) => {
switch ( action.type ){
case 'ADD':
return [...state,action.payload] ;
default :
return state ;
}
}
let reducer = combineReducer({
personReducer ,
todoReducer
})
let store = createStore(reducer) ;
store.dispatch({ type : 'NAME' , payload : 'Jeno' });
store.dispatch({ type : 'AGE' , payload : 22 });
store.dispatch({ type : 'ADD' , payload : 'Coding' });
console.log(store.getState());

接下來實作combineReducer的部分,首先我們先回傳一個函式來代表合併的reducer

1
2
3
4
5
const combineReducer = () => {
return function(){
}
}

接下來一樣如同一般的reducer一樣接受stateaction,在這邊創建一個新的newState回傳。

1
2
3
4
5
6
const combineReducer = (reducers) => {
return function(state = {} , action){
let newState = {};
return newState ;
}
}

接下來就是遍歷整個reducers,並將傳入中action和相對應的state傳入該reducer得到的狀態放在newState並回傳newState即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* 這時候 reducers 物件的內容
reducers = {
personReducer : function( state = {} , action ){
switch ( action.type ){
case 'NAME':
return {...state, name : action.payload };
case 'AGE':
return {...state, age : action.payload } ;
default :
return state ;
}
},
todoReducer : function( state = [] , action ){
switch ( action.type ){
case 'ADD':
return [...state,action.payload] ;
default :
return state ;
}
}
}
*/
const combineReducer = (reducers) => {
return function(state = {} , action){
let newState = {};
for ( let key in reducers ) {
newState[key] = reducers[key](state[key],action) ;
}
return newState ;
}
}


bindActionCreators

bindActionCreators可以直接把創造action的函式直接和dispatch綁在一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const setPersonName = (name) => {
return {
type : 'NAME' ,
payload : name
}
}
const setPersonAge = (age) => {
return {
type : 'AGE' ,
payload : age
}
}
const addTodo = (text) => {
return {
type : 'ADD' ,
payload : text
}
}
const boundActionCreators = bindActionCreators({ setPersonName , setPersonAge , addTodo },store.dispatch) ;
boundActionCreators.addTodo('Coding') ;
boundActionCreators.setPersonAge(22);
boundActionCreators.setPersonName('Jeno');
console.log(store.getState());

接下來是實作的部分,我們首先創建一個空物件boundActionCreators並回傳。

1
2
3
4
const bindActionCreators = (actionCreators,dispatch) => {
let boundActionCreators = {} ;
return boundActionCreators ;
}

再來遍歷actionCreators並把相對應的key宣告為函式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* 這時 actionCreators 的內容
actionCreators = {
setPersonName : function(name){
return {
type : 'NAME' ,
payload : name
}
},
setPersonAge : function(age){
return {
type : 'AGE' ,
payload : age
}
},
addTodo : function(text){
return {
type : 'ADD' ,
payload : text
}
}
}
*/
const bindActionCreators = (actionCreators,dispatch) => {
let boundActionCreators = {} ;
for ( let key in actionCreators ){
boundActionCreators[key] = () => {
}
}
return boundActionCreators ;
}

該函式的內容則是直接利用dispatch去發出該函式回傳的action

1
2
3
4
5
6
7
8
9
const bindActionCreators = (actionCreators,dispatch) => {
let boundActionCreators = {} ;
for ( let key in actionCreators ){
boundActionCreators[key] = (payload) => {
dispatch(actionCreators[key](payload)) ;
}
}
return boundActionCreators ;
}


applyMiddleware

applyMiddleware可以將dispacth包裝起來,如同洋蔥一樣必須一層一層透過middlewares才會到最後的store.dispatch

1
2
3
4
5
6
7
8
9
10
11
12
13
const middleware1 = (middlewareAPI) => (next) => (action) => {
console.log('I am middleware 1, my next is ',next) ;
return next(action) ;
}
const middleware2 = (middlewareAPI) => (next) => (action) => {
console.log('I am middleware 2, my next is ',next) ;
return next(action);
}
let store = createStore(reducer,{},applyMiddleware(middleware1,middleware2)) ;
store.dispatch({ type : 'NAME' , payload : 'Jeno '}) ;
console.log(store.getState());

首先假設我們現在只有一個middlewaremiddleware傳入兩個參數dispatchaction如下,代表希望dispatch可以透過這個middleware做處理。

1
2
3
4
5
6
7
const middleware = (dispatch,action) => {
console.log('I am middleware, before dispatch');
dispatch(action);
console.log('I am middleware, after dispatch');
}
let store = createStore(reducer,{},middleware) ;

我們先創建另一個函式專門針對含有middleware的做處理,若要使用middleware則必須使用該函式。

1
2
3
4
5
6
7
8
9
10
11
const createStoreWithMiddleWare = (reducer,state,middleware) => {
}
const middleware = (dispatch,action) => {
console.log('I am middleware, before dispatch');
dispatch(action);
console.log('I am middleware, after dispatch');
}
let store = createStoreWithMiddleWare(reducer,{},middleware) ;

接下來實作createStoreWithMiddleWare的部分,先使用原本createStore創造store,將dispatch修改為將原先的dispatchaction傳入middleware做處理,再將修改後的store回傳。

1
2
3
4
5
6
7
8
9
10
const createStoreWithMiddleWare = (reducer,state,middleware) => {
let store = createStore(reducer,state) ;
let dispatch = function(action){
middleware(store.dispatch,action) ;
}
return {
...store,
dispatch
}
}

我們可以在createStore做處理,如此一來就不需要呼叫兩個不同的函式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const createStore = (reducer,state,middleware) => {
if ( middleware !== undefined ){
return createStoreWithMiddleWare(reducer,state,middleware) ;
}
let nowReducer = reducer ;
let nowState = state ;
let nowListeners = [] ;
const getState = () => {
return nowState ;
}
const dispatch = (action) => {
nowState = nowReducer(nowState,action) ;
nowListeners.forEach((listener) => listener());
}
const subscribe = (listener) => {
nowListeners.push(listener) ;
return function(){
nowListeners.splice(nowListeners.indexOf(listener),1);
}
}
const replaceReducer = (reducer) => {
nowReducer = reducer ;
dispatch({ type : 'INIT' }) ;
}
dispatch({ type : 'INIT' }) ;
return {
getState ,
dispatch ,
subscribe ,
replaceReducer
}
}
let store = createStore(reducer,{},middleware) ;

若我們想要傳入多個middlewares,我們可以修改每一個middleware成新的函式,將下一個middleware帶進去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const createStoreWithMiddleWare = (reducer,state,middlewares) => {
let store = createStore(reducer,state) ;
let newMiddleware = [...middlewares,store.dispatch] ;
for ( let i = 0 ; i < newMiddleware.length - 1 ; i ++ ){
let fn = newMiddleware[i] ;
newMiddleware[i] = function(action,dispatch){
fn(action,newMiddleware[i+1]) ;
}
}
let dispatch = function(action){
newMiddleware[0](action,newMiddleware[1]) ;
}
return {
...store,
dispatch
}
}
const middleware1 = (action,dispatch) => {
console.log('I am middleware 1, before dispatch');
dispatch(action);
console.log('I am middleware 1, after dispatch');
}
const middleware2 = (action,dispatch) => {
console.log('I am middleware 2, before dispatch');
dispatch(action);
console.log('I am middleware 2, after dispatch');
}

我們在這邊把actiondispatch參數對調,是為了因應dispatch(action)第一個參數是action的原因。

或者,我們也可以在這邊先將middleware函式Curry化,如此一來我們就可以先傳入store.patch,之後再傳入action,以便於直接搭配compose來使用。

關於Curry可以看我另一篇文章:[JavaScript] Curry
關於Compose可以看我的另一篇文章:[JavaScript] Compose 和 Pipe

1
2
3
4
5
6
7
8
9
10
11
const createStoreWithMiddleWare = (reducer,state,middleware) => {
let store = createStore(reducer,state) ;
let dispatch = store.dispatch ;
dispatch = function(action){
middleware(store.dispatch)(action) ;
}
return {
...store,
dispatch
}
}

接著修改middleware的函式結構。

1
2
3
4
5
const middleware = (dispatch) => (action) => {
console.log('I am middleware 1, before dispatch');
dispatch(action);
console.log('I am middleware 1, after dispatch');
}

如果要傳入多個middlewares

1
2
3
4
5
6
7
8
9
10
11
12
13
const middleware1 = (dispatch) => (action) => {
console.log('I am middleware 1, before dispatch');
dispatch(action);
console.log('I am middleware 1, after dispatch');
}
const middleware2 = (dispatch) => (action) => {
console.log('I am middleware 2, before dispatch');
dispatch(action);
console.log('I am middleware 2, after dispatch');
}
let store = createStore(reducer,{},[middleware1,middleware2]) ;

修改createStoreWithMiddleWare,先compose所有的middlewares再傳入store.dispatch

1
2
3
4
5
6
7
8
9
10
11
12
const createStoreWithMiddleWare = (reducer,state,middlewares) => {
let store = createStore(reducer,state) ;
let dispatch = store.dispatch ;
let composedMiddleWare = compose(...middlewares)(store.dispatch);
dispatch = function(action){
composedMiddleWare(action) ;
}
return {
...store,
dispatch
}
}

帶入store.dispatchComposeCurry後的middlewares,可以經由每次回傳一個函式到上一個middleware,如此一來就可以組合成一個大的middleware使傳入的action可以透過middleware1->middleware2->store.dispatch

接著,假設我們現在必須對特定的action做處理,例如

1
2
3
4
5
6
7
8
9
store.dispatch(() => {
setTimeout(()=>{
dispatch({
type : 'AGE' ,
payload : 22
}) ;
console.log(getState());
},1000)
}) ;

發現在action裡面的函式並沒有dispatchgetState可以使用,因此必須傳這兩個進來。

1
2
3
4
5
6
7
8
9
10
11
store.dispatch((middlewareAPI) => {
let dispatch = middlewareAPI.dispatch ;
let getState = middlewareAPI.getState ;
setTimeout(()=>{
dispatch({
type : 'AGE' ,
payload : 22
}) ;
console.log(getState());
},1000)
}) ;

如此一來修改createStoreWithMiddleWare,在這邊我們傳入帶dispatchgetStatemiddlewareAPI到每個middleware,注意在這邊帶的dispatch必須是修改過後的dispatch而不是store.dispatch,才可以確保每次dispatch都會從第一個middleware開始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const createStoreWithMiddleWare = (reducer,state,middlewares) => {
let store = createStore(reducer,state) ;
let dispatch = store.dispatch ;
let middlewareAPI = {
getState : store.getState ,
dispatch : (action) => dispatch(action)
}
let newMiddlewares = middlewares.map((middleware)=>middleware(middlewareAPI));
let composedMiddleWare = compose(...newMiddlewares)(store.dispatch);
dispatch = function(action){
composedMiddleWare(action) ;
}
return {
...store,
dispatch
}
}

再來我們必須修改middleware的架構,再這邊我們把middleware2修改成可以處理action是函式的狀況,同時這也是redux-thunk實作原理。

為了避免dispatch搞混,我們將前往下一個middlewaredispatch改為next

1
2
3
4
5
6
7
8
9
10
11
12
13
const middleware1 = (middlewareAPI) => (next) => (action) => {
console.log('I am middleware 1, going to next');
let result = next(action);
}
const middleware2 = (middlewareAPI) => (next) => (action) => {
console.log('I am middleware 2, going to next');
if ( typeof action === 'function' ){
action(middlewareAPI) ;
} else {
let result = next(action);
}
}

因此每當我們呼叫一般的物件action,則會通過middleware1->middleware2->store.dispatch

action是函式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let store = createStore(reducer,{},[middleware1,middleware2]) ;
console.log(store.getState());
store.dispatch({ type : 'NAME' , payload : 'Jeno '}) ;
console.log(store.getState());
store.dispatch((middlewareAPI) => {
let dispatch = middlewareAPI.dispatch ;
let getState = middlewareAPI.getState ;
setTimeout(()=>{
dispatch({
type : 'AGE' ,
payload : 22
}) ;
console.log(getState());
},1000)
}) ;

流程則是middleware1->middleware2->middleware1->middleware2->store.dispatch

如此一來處理middlewares的函式其實差不多了,我們把參數傳遞方式改一下並且改createStoreWithMiddleWareapplyMiddleware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const applyMiddleware = (reducer,state,middlewares) => {
let store = createStore(reducer,state) ;
let dispatch = store.dispatch ;
let middlewareAPI = {
getState : store.getState ,
dispatch : (action) => dispatch(action)
}
let newMiddlewares = middlewares.map((middleware)=>middleware(middlewareAPI));
let composedMiddleWare = compose(...newMiddlewares)(store.dispatch);
dispatch = function(action){
composedMiddleWare(action) ;
}
return {
...store,
dispatch
}
}
let store = createStore(reducer,{},applyMiddleware(middleware1,middleware2)) ;

接著改寫applyMiddleware的結構,並讓它可以傳reducerstate進來。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const applyMiddleware = (...middlewares) => (reducer,state) => {
let store = createStore(reducer,state) ;
let dispatch = store.dispatch ;
let middlewareAPI = {
getState : store.getState ,
dispatch : (action) => dispatch(action)
}
let newMiddlewares = middlewares.map((middleware)=>middleware(middlewareAPI));
let composedMiddleWare = compose(...newMiddlewares)(store.dispatch);
dispatch = function(action){
composedMiddleWare(action) ;
}
return {
...store,
dispatch
}
}

createStore的部分也做修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const createStore = (reducer,state,enhancer) => {
if ( enhancer !== undefined ){
return enhancer(reducer,state) ;
}
let nowReducer = reducer ;
let nowState = state ;
let nowListeners = [] ;
const getState = () => {
return nowState ;
}
const dispatch = (action) => {
nowState = nowReducer(nowState,action) ;
nowListeners.forEach((listener) => listener());
}
const subscribe = (listener) => {
nowListeners.push(listener) ;
return function(){
nowListeners.splice(nowListeners.indexOf(listener),1);
}
}
const replaceReducer = (reducer) => {
nowReducer = reducer ;
dispatch({ type : 'INIT' }) ;
}
dispatch({ type : 'INIT' }) ;
return {
getState ,
dispatch ,
subscribe ,
replaceReducer
}
}

Redux會再把createStore傳進applyMiddleware裡,如此一來就不需要createStore這段程式了。